package br.mikhas.container.annotation;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import br.mikhas.container.config.BeanDefinition;
import br.mikhas.container.config.ResourceReferenceDependency;
import br.mikhas.container.config.StandadBeanDefinition;
import br.mikhas.container.config.Dependency.DependencyTarget;
import br.mikhas.reflector.ClassProxy;
import br.mikhas.reflector.Reflect;
import br.mikhas.reflector.annotation.AnnotationProxy;
import br.mikhas.reflector.field.FieldProxy;
import br.mikhas.reflector.method.MethodProxy;
import br.mikhas.util.Assert;
import br.mikhas.util.CollectionUtils;
import br.mikhas.util.StringUtils;

/**
 * Extracts and configures the bean definition using data on class annotations
 * 
 * @author Mikhail Domanoski
 * 
 */
class AnnotatedBeanDefinitionExtractor implements BeanDefinitionExtractor {

	private static final Logger log = LoggerFactory
			.getLogger(AnnotatedBeanDefinitionExtractor.class);

	private static final boolean debug = log.isDebugEnabled();

	/**
	 * The definition extractos
	 */
	private List<ChainedDefinitionExtractor> extractors = new ArrayList<ChainedDefinitionExtractor>();

	/**
	 * The bean definition provider which is using this extractor
	 */
	private AnnotatedBeanProvider provider;

	public AnnotatedBeanDefinitionExtractor(
			AnnotatedBeanProvider annotatedBeanProvider) {
		this.provider = annotatedBeanProvider;
	}

	/**
	 * Extracts the bean definition from a bean class by executing a chain of
	 * extractions.
	 */
	@Override
	public final BeanDefinition extractDefinition(Class<?> cls) {
		if (debug)
			log.debug("Extracting BeanDefinition from class: " + cls);
		BeanDefinition def = doExtractDefinition(cls);

		if (!CollectionUtils.isEmpty(this.extractors)) {
			if (debug)
				log.debug("Running definition extraction chain");

			for (ChainedDefinitionExtractor extractor : this.extractors)
				def = extractor.extractDefinition(cls, def, provider);
		}

		return def;
	}

	/**
	 * Extracts bean definition reading the class annotations
	 */

	AnnotatedBeanDefinition doExtractDefinition(Class<?> cls) {
		ClassProxy<?> reflect = Reflect.on(cls);

		if (reflect.isAnnotated(Resource.class)) {

			String name;

			AnnotationProxy resource = reflect.annotation(Resource.class);

			name = resource.field("value").get();
			if (name.isEmpty()) {
				name = cls.getName();
			}

			AnnotatedBeanDefinition definition = new AnnotatedBeanDefinition(
					resource.getAnnotation(), name, cls);

			if (reflect.isAnnotated(Scope.class)) {
				String scopeName = reflect.annotation(Scope.class).field(
						"value").get();

				definition.setScope(scopeName);
			}

			getDependencies(reflect, definition);

			return definition;
		} else {
			return null;
		}
	}

	/**
	 * Gets the bean dependencies from the class definition
	 * 
	 * @param cls
	 *            The class to the analysed
	 * @param definition
	 *            The current bean definition
	 */
	private void getDependencies(ClassProxy<?> cls,
			StandadBeanDefinition definition) {

		FieldProxy[] fields = cls.fields();

		// Extract attributes dependencies and references
		for (FieldProxy field : fields) {
			if (field.isAnnotated(Resource.class)) {
				String resource = field.annotation(Resource.class).field(
						"value").get();

				ResourceReferenceDependency dependency = new ResourceReferenceDependency(
						field.type(), DependencyTarget.Attribute);

				dependency.setPropertyName(field.name());

				if (!StringUtils.isEmpty(resource)) {
					dependency.setReferenceName(resource);
				}

				definition.addDependency(dependency);
			}
		}

		MethodProxy[] methods = cls.methods();
		// Extract methods dependencies and references
		for (MethodProxy method : methods) {
			if (method.isAnnotated(Resource.class)) {
				String resource = method.annotation(Resource.class).field(
						"value").get();

				ResourceReferenceDependency dependency = new ResourceReferenceDependency(
						method.type(), DependencyTarget.Property);

				if (!StringUtils.isEmpty(resource)) {
					dependency.setReferenceName(resource);
				}

				definition.addDependency(dependency);
			}
		}
	}

	/**
	 * Adds an Extractor to the chain
	 * 
	 * @param extractor
	 *            the new chained extractor to be added to the chain
	 */
	public void addExtractor(ChainedDefinitionExtractor extractor) {
		Assert.notNull(extractor);
		this.extractors.add(extractor);
	}
}
